iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
自我挑戰組

韌體工程師的不只 0x10 個問題系列 第 1

Day 1:C 語言從原始碼到執行檔過程

  • 分享至 

  • xImage
  •  

前情提要:本系列文章皆使用 GCC 編譯器,在 Linux 作業系統可直接用套件管理工具安裝;使用 macOS 者可參考此連結安裝命令列工具取得;使用 Windows 作業系統者可參考此連結安裝 MinGW。

當我們寫一段 C 語言程式碼,要變成電腦上真正可以執行的程式,共要經過下列四大步驟:

  1. 由預處理器(preprocessor)處理所有 # 開頭的指令。
  2. 由編譯器(compiler)轉成組合語言。
  3. 由組譯器(assembler)轉成二進位機器碼。
  4. 由連結器(linker)連接所有機器碼。

圖示如下:https://ithelp.ithome.com.tw/upload/images/20260801/20178513GgMMLoArDi.png

範例

(以下指令皆以 Windows 作業系統為例)

以 C 語言撰寫一檔名為 hello.c 的 Hello, World! 程式碼如下:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

這就是「原始程式(source program)」。

預處裡 Preprocess

經過下列預處理指令:

gcc -E hello.c -o hello.i

會產生下列「預處理過的原始程式(modified source program)」,檔名為 hello.i

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>" 

# 省略超過四百行...

# 3 "hello.c"
int main() {
    printf("Hello, World!\n");
    return 0;
}

最下面的主程式 main() 還在,但原本的 #include <stdio.h> 被大幅展開,<stdio.h> 裡包含的其他原始碼也都被一層層展開,因此原始碼僅 6 行的程式碼大幅膨脹至超過 400 行。

預處理的主要功能是讓程式知道有 printf() 這個寫在 <stdio.h> 的函式可以用,等到最後連結時,再把 printf() 對應的機器語言一起組成執行檔。

編譯 Compile

這些預處理器被展開的原始碼再經過下列編譯指令:

gcc -S hello.i -o hello.s

產生如下的組合語言程式(assembly program),檔名為 hello.s

	.file	"hello.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "Hello, World!\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB10:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$16, %esp
	call	___main
	movl	$LC0, (%esp)
	call	_puts
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE10:
	.ident	"GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
	.def	_puts;	.scl	2;	.type	32;	.endef

編譯之後產生的組合語言已經開始在寫機器的執行步驟,當中可看到如 %ebp%esp 等暫存器調用相關指令。

編譯 Assemble

組合語言仍是人類(經過訓練後)能看得懂的文字檔,但只要再用下列指令進行組譯:

gcc -c hello.s -o hello.o

組譯後產生的目標檔(relocatable object program)為由 0 跟 1 組成的機器碼,檔名為 hello.o,要查看其二進位內容可用如下指令:

xxd -b hello.o

可得:

00000006: 00000000 00000000 10111100 00000001 00000000 00000000  ......
0000000c: 00010010 00000000 00000000 00000000 00000000 00000000  ......
00000012: 00000100 00000001 00101110 01110100 01100101 01111000  ...tex

# 省略超過一百行...

0000032a: 01100001 01101101 01100101 00000000                    ame.

連結 Link

最後再用以下指令:

gcc hello.o -o hello.exe

可得一名為 hello.exe 的執行檔(executable object program),與 hello.o 一樣可用以下指令檢視其二進位內容:

xxd -b hello.exe

內容如下:

00000000: 01001101 01011010 10010000 00000000 00000011 00000000  MZ....
00000006: 00000000 00000000 00000100 00000000 00000000 00000000  ......

# 省略超過六千七百行...

00009f36: 01110111 01110010 01101001 01110100 01100101 00000000  write.

除了主程式外,在預處理階段也已經展開 <stdio.h>,讓程式知道有已經寫在標準輸入輸出函式庫的 printf() 函式宣告可以用。

因此,hello.exe 不是只有 main() 主程式的目標檔機器碼,還有來自函式庫的 printf() 目標檔機器碼(可想成 printf.o),兩者結合才有辦法順利執行 printf("Hello, World!\n"); 的功能。

補充:從原始碼直接產生不同階段檔案

上述是從原始碼一步步變成執行檔的過程,但 GCC 指令也允許我們跳步驟產生不同檔案,例如:

從原始碼直接變成組合語言程式:

gcc -S hello.c -o hello.s

從原始碼直接變成目標檔:

gcc -c hello.c -o hello.o

從原始檔直接變成執行檔:

gcc hello.c -o hello.exe

最後一個指令相對常用,而若沒有提供 -o 參數,變成:

gcc hello.c

預設產生的執行檔名為 a.exe (或 a.out)。


系列文
韌體工程師的不只 0x10 個問題1
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言